using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.SessionState;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Traces user activity in a page and the duration of server-side page execution.
    /// </summary>
    public class PageActivityTrace
    {
        static readonly IActor anonymous = new Actor(0, "Anonymous");

        #region Fields.

        IActor actor;
        RequestFilter filter;
        PageTraceScope scope;
        Dictionary<string, object> trace = new Dictionary<string, object>();
        DateTime startTime;
        DateTime? endTime;

        #endregion

        #region CTor.

        /// <summary>
        /// Initializes a new instance of <b>PageActivityTrace</b> class.
        /// </summary>
        public PageActivityTrace()
        {
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets optional actor whose activity is traced.
        /// </summary>
        /// <value><b>IActor</b> object or <b>null</b>. Default is <b>null</b>.</value>
        /// <remarks>If <b>null</b> trace is logged for <b>Anonymous</b> user.</remarks>
        public virtual IActor Actor
        {
            get { return actor; }
            set { actor = value; }
        }

        /// <summary>
        /// Gets or sets optional object which checks whether a page matches filter criteria.
        /// </summary>
        /// <value><b>RequestFilter</b> object or <b>null</b>. Default is <b>null</b>.</value>
        /// <remarks>If <b>null</b> no filtering occures.</remarks>
        public virtual RequestFilter Filter
        {
            get { return filter; }
            set { filter = value; }
        }

        /// <summary>
        /// Gets or sets optional object which specifies the page/request elements to trace.
        /// </summary>
        /// <value><b>PageTraceScope</b> object or <b>null</b>. Default is <b>null</b>.</value>
        /// <remarks>If <b>null</b> <see cref="PageTraceScope.Default"/> scope is used.</remarks>
        public virtual PageTraceScope Scope
        {
            get { return scope; }
            set { scope = value; }
        }

        /// <summary>
        /// Gets the traced variables.
        /// </summary>
        protected virtual Dictionary<string, object> Trace
        {
            get { return trace; }
        }

        /// <summary>
        /// The time recorded at the start of the trace.
        /// </summary>
        /// <value><b>DateTime</b>.</value>
        public virtual DateTime StartTime
        {
            get { return startTime; }
            protected set { startTime = value; }
        }

        /// <summary>
        /// The time recorded at the end of the trace.
        /// </summary>
        /// <value><b>DateTime</b> or <b>null</b> if the trace hasn't ended.</value>
        public virtual DateTime? EndTime
        {
            get { return endTime; }
            protected set { endTime = value; }
        }

        /// <summary>
        /// Gets the duration of the traced request.
        /// </summary>
        /// <value>Difference between <see cref="EndTime"/> and <see cref="StartTime"/> if the trace has ended.
        /// If the trace is still active then the difference between <see cref="Time.Now"/> and <see cref="StartTime"/>.</value>
        public virtual TimeSpan Duration
        {
            get
            {
                TimeSpan requestDuration = (this.EndTime != null) ? this.EndTime.Value - this.StartTime : Time.Now - this.StartTime;
                return requestDuration;
            }
        }

        #endregion

        #region Helper methods.

        /// <summary>
        /// Gets the control which initiated post-back.
        /// </summary>
        /// <param name="page">Page to inspect.</param>
        /// <returns>Control which initiated post-back. <b>null</b> if no post-back occured or if post-back control cannot be found.</returns>
        public static Control GetPostBackControl(Page page)
        {
            if (!page.IsPostBack)
                return null;

            Control control = null;
            string eventTarget = page.Request.Params["__EVENTTARGET"];
            if (!string.IsNullOrEmpty(eventTarget))
            {
                control = page.FindControl(eventTarget);
            }
            else
            {
                // If __EVENTTARGET is null, the control is a button type and we need to 
                // iterate over the form collection to find it.
                foreach (string httpVariable in page.Request.Form)
                {
                    // ASP.NET appends .x and .y suffixes to ImageButton control IDs.
                    bool isImageButton = httpVariable.EndsWith(".x") || httpVariable.EndsWith(".y");
                    if (isImageButton)
                    {
                        string controlId = httpVariable.Substring(0, httpVariable.Length - 2);
                        ImageButton imageBtn = page.FindControl(controlId) as ImageButton;
                        if (imageBtn != null)
                            control = imageBtn;
                    }
                    else
                    {
                        Button btn = page.FindControl(httpVariable) as Button;
                        if (btn != null)
                            control = btn;
                    }

                    if (control != null)
                        break;
                }
            }

            return control;
        }

        #endregion

        #region StartTrace.

        /// <summary>
        /// Hooks the current instance to the current page if available and sets the <see cref="StartTime"/> property.
        /// </summary>
        /// <remarks><para>Trace is attached to current page if the pages matches the criteria specified in the
        /// <see cref="Filter"/> property. Only elements defined by <see cref="Scope"/> property are traced.</para>
        /// <para>The trace should be attached as soon as page instance is available. This must occur before the page 
        /// is loaded. <see cref="HttpApplication.PreRequestHandlerExecute"/> event is the best place, but the following
        /// <see cref="Page"/> events will also work: <b>PreInit</b>, <b>Init</b>, <b>Load</b>.</para>
        /// <para>The page is processed in <see cref="Page.LoadComplete"/> event.</para>
        /// </remarks>
        public virtual void StartTrace()
        {
            Page page = HttpContext.Current.Handler as Page;
            HttpRequest req = HttpContext.Current.Request;
            bool cannotHook = (page == null) || (req == null);
            if (cannotHook)
                return;

            bool shouldTraceUrl = (this.Filter != null) ? this.Filter.MatchesFilter(req) : true;
            if (shouldTraceUrl)
            {
                this.StartTime = Time.Now;
                page.LoadComplete += new EventHandler(page_LoadComplete);
                page.Unload += new EventHandler(page_Unload);
            }
        }

        #endregion

        #region page_LoadComplete, ProcessPage.

        private void page_LoadComplete(object sender, EventArgs e)
        {
            ProcessPage((Page)sender);
        }

        /// <summary>
        /// Processes/logs page contents.
        /// </summary>
        /// <param name="page">Loaded page to trace. This is the page to which the trace was attached in <see cref="StartTrace"/> method.</param>
        /// <remarks><para>The method is invoked <see cref="Page.LoadComplete"/> event. 
        /// Traced variables are appended to <see cref="Trace"/> collection.</para>
        /// <para>The method first checks whether the page match criterira in the <see cref="Filter"/> property and then it 
        /// appends elements specified in the <see cref="Scope"/> to <see cref="Trace"/> property.</para>
        /// </remarks>
        protected virtual void ProcessPage(Page page)
        {
            bool matchesFilter = (this.Filter != null) ? this.Filter.MatchesFilter(page) : true;
            if (!matchesFilter)
            {
                UnregisterLogOnPageUnload(page);
                return;
            }

            // Reserve first slot in trace dictionary for "Duration".
            this.Trace["Duration"] = null;

            PageTraceScope settings = this.Scope ?? PageTraceScope.Default;
            LogSessionId(settings, trace);
            LogRawUrl(page.Request, settings, this.Trace);            
            LogHttpParams(page.Request, settings, this.Trace);
            LogHttpHeaders(page.Request, settings, this.Trace);            
            LogPostBackControl(page, this.Trace);
            LogFormVariables(page.Request, settings, this.Trace);            
        }

        private static void LogSessionId(PageTraceScope settings, Dictionary<string, object> trace)
        {
            if (settings.SessionIdLogged)
                trace["SessionID"] = HttpContext.Current.Session.SessionID;
        }

        private static void LogRawUrl(HttpRequest req, PageTraceScope settings, Dictionary<string, object> trace)
        {
            if (settings.RawUrlLogged)
                trace["RawUrl"] = req.RawUrl;
        }

        private static void LogPostBackControl(Page page, Dictionary<string, object> trace)
        {
            if (page.IsPostBack)
            {
                Control submitControl = PageActivityTrace.GetPostBackControl(page);
                bool isDeterminedSubmitCtl = (submitControl != null) && !string.IsNullOrEmpty(submitControl.ID);
                string ctlId = (isDeterminedSubmitCtl) ? submitControl.ID : page.Request["__EVENTTARGET"];
                if (!string.IsNullOrEmpty(ctlId))
                    trace.Add("PostBackControl", ctlId);
            }
        }

        private static void LogFormVariables(HttpRequest req, PageTraceScope settings, Dictionary<string, object> trace)
        {
            if (settings.FormVariablesFilter != null)
            {
                foreach (string formVariable in req.Form)
                {
                    bool logCurrVariable = settings.FormVariablesFilter.IsMatch(formVariable);
                    if (logCurrVariable)
                        trace[formVariable] = req.Form[formVariable];
                }
            }
        }

        private static void LogHttpHeaders(HttpRequest req, PageTraceScope settings, Dictionary<string, object> trace)
        {
            if (!ArrayUtil.IsNullOrEmpty(settings.HttpHeaders))
            {
                foreach (string header in settings.HttpHeaders)
                    trace[header] = req.Headers[header];
            }
        }

        private static void LogHttpParams(HttpRequest req, PageTraceScope settings, Dictionary<string, object> trace)
        {
            if (!ArrayUtil.IsNullOrEmpty(settings.HttpParams))
            {
                foreach (string param in settings.HttpParams)
                    trace[param] = req.Params[param];
            }
        }

        #endregion

        #region page_Unload, StopTrace.

        void page_Unload(object sender, EventArgs e)
        {
            StopTrace((Page)sender);
        }

        /// <summary>
        /// Unregisters <b>Page.Unload</b> event handler which invokes the <see cref="StopTrace"/> method.
        /// </summary>
        /// <param name="page">Traced page.</param>
        protected void UnregisterLogOnPageUnload(Page page)
        {
            page.Unload -= page_Unload;
        }

        /// <summary>
        /// Stops the current trace, sets the <see cref="EndTime"/> property and logs the requested information.
        /// </summary>
        /// <param name="page">Unloaded page. This is the page to which the trace was attached in <see cref="StartTrace"/> method.</param>
        /// <remarks><para>The method is invoked in <b>Page.Unload</b> event. 
        /// Traced variables are appended to <see cref="Trace"/> collection.</para>
        /// <para>Information is logged in <b>Activity</b> category under <b>PageActivityTrace</b> name (see 
        /// <see cref="Log.Activity(string, string, IActor)"/> method). <b>AppWarning</b> is logged if the duration 
        /// of page execution exceeds the value set in <see cref="PageTraceScope.DurationWarningThreshold"/>.</para>
        /// </remarks>
        protected virtual void StopTrace(Page page)
        {
            this.EndTime = Time.Now;
            this.Trace["Duration"] = this.Duration;
            IActor currUser = actor ?? anonymous;
            Log.Activity("PageActivityTrace", page.GetType().ToString(), currUser, this.Trace);

            PageTraceScope settings = this.Scope ?? PageTraceScope.Default;
            bool logWarningBecauseItTakesToMuchTime = (this.Duration > settings.DurationWarningThreshold);
            if (logWarningBecauseItTakesToMuchTime)
                Log.AppWarning("PageActivityTrace", page.GetType().ToString() + " - DurationWarningThreshold exceeded.", this.Trace);
        }

        #endregion
    }
}